#!/usr/bin/perl

package Swignition::Export::Contact;
use Swignition::Export;
use Swignition::Export::Address;
use Swignition::Export::Point;
use Swignition::Export::Taxonomy;
use Swignition::uF::hCard;
use HTML::Entities qw(encode_entities_numeric);
use JSON 2.0;
use MIME::Base64;
use strict;

# Reference:
# http://www.ibiblio.org/hhalpin/homepage/notes/vcardtable.html
# (Though that page does contain a number of mistakes, such as equating a vCard
# "TITLE" [a job title] with a FOAF "title" [e.g. Mr, Mrs].)

sub is
{
	my $node  = shift;
	my $graph = shift;
	
	my @types;
	push @types, 'http://xmlns.com/foaf/0.1/Person';
	push @types, 'http://xmlns.com/foaf/0.1/Organisation';
	push @types, 'http://xmlns.com/foaf/0.1/Project';
	push @types, 'http://xmlns.com/foaf/0.1/Group';
	push @types, 'http://xmlns.com/foaf/0.1/Agent';
	push @types, 'http://www.w3.org/2001/vcard-rdf/3.0#VCARD';
	push @types, 'http://www.w3.org/2006/vcard/ns#VCard';
	push @types, 'http://www.w3.org/2000/10/swap/pim/contact#Person';
	push @types, 'http://www.w3.org/2000/10/swap/pim/contact#Male';
	push @types, 'http://www.w3.org/2000/10/swap/pim/contact#Female';
	push @types, 'http://www.w3.org/2000/10/swap/pim/contact#SocialEntity';
	push @types, 'urn:ietf:rfc:2426#Vcard';
	push @types, 'http://xmlns.com/wot/0.1/User';
	
	foreach my $type (@types)
	{
		return 1 if $node->a($type, [$graph]);
	}
}

sub outputs
{
	return [
		{
			code  => 'VCF',
			name  => 'vCard',
			type  => 'text/directory',
			multi => 1
		},
		{
			code  => 'Name',
			name  => 'Contact Name',
			type  => 'text/plain',
			multi => 1
		},
		{
			code  => 'JSON',
			name  => 'jCard',
			type  => 'text/x-json',
			multi => 1
		}
	];
}

sub multiJSON
{
	my $triples = shift;
	my @rv;
		
	my @keysTriples;
	my $page;
	foreach my $s (keys %$triples)
	{
		if (defined $triples->{$s} && is($triples->{$s}))
		{
			$page = $triples->{$s}->{page} unless ($page);
			push @keysTriples, $s;
		}
	}
	return '[]' unless (defined $keysTriples[0]);
	
	my %orderOf;
	my $i = 1;
	foreach my $hc (@{ $page->{uF}->{hCard} })
	{
		$orderOf{ $hc->uri } = $i++;
	}
	my @sortedKeysTriples = sort {
		return -1 if ($orderOf{$a} == 0);
		return  1 if ($orderOf{$b} == 0);
		return $orderOf{$a} - $orderOf{$b};
	} @keysTriples;
	
	foreach my $s (@sortedKeysTriples)
	{
		my %r = _outJSON($triples, $s);
		push @rv, \%r;
	}
 	return to_json(\@rv, {utf8 => 1, pretty => 1, canonical => 1, convert_blessed => 1})."\n";
}

sub multiVCF
{
	my $page  = shift;
	my $graph = shift;
	my $rv    = '';

	my @people;
	foreach my $node (@{ $graph->get_nodes })
	{
		if (is($node, $graph))
		{
			push @people, $node;
		}
	}
	
	return '' unless (defined $people[0]);

	my %orderOf;
	my $i = 1;
	foreach my $hc (@{ $page->{uF}->{hCard} })
	{
		$orderOf{ $hc->uri } = $i++;
	}	
	my @sortedPeople = sort {
		return -1 if ($orderOf{''.$a->identity} == 0);
		return  1 if ($orderOf{''.$b->identity} == 0);
		return $orderOf{''.$a->identity} - $orderOf{''.$b->identity};
	} @people;

	foreach my $p (@sortedPeople)
	{
		$rv .= outVCF($page, $graph, $p->identity);
	}
	return $rv;
}

sub outJSON
{
	my %json = _outJSON(@_);
 	return to_json(\%json, {utf8 => 1, pretty => 1, canonical => 1, convert_blessed => 1})."\n";
}

sub _outJSON
{
	my $triples = shift;
	my $subject = shift;
	my $S = findSubject($triples, $subject);
	my %rv;

	# UID
	my $uid = firstOf($S, 'urn:ietf:rfc:2426#uid',
		'http://www.w3.org/2006/vcard/ns#uid',
		'http://www.w3.org/2001/vcard-rdf/3.0#UID',
		'http://purl.org/dc/terms/identifier',
		'http://purl.org/dc/elements/1.1/identifier');
	$rv{uid} = "$uid"
		if ($uid);

	my $n = outN($triples, $subject, 'jcard');
	$rv{n} = $n if ($n);
	
	# Formatted name
	my $fn = firstOfLiteral($S,
		['urn:ietf:rfc:2426#fn',
		'http://www.w3.org/2006/vcard/ns#fn',
		'http://www.w3.org/2001/vcard-rdf/3.0#FN',
		'http://xmlns.com/foaf/0.1/fullName',
		'http://xmlns.com/foaf/0.1/name',
		'http://open.vocab.org/terms/fullName',
		'http://open.vocab.org/terms/informalName',
		'http://open.vocab.org/terms/formalName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
		'http://purl.org/dc/terms/title',
		'http://purl.org/dc/elements/1.1/title',
		'http://www.w3.org/2000/01/rdf-schema#label'],
		['http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);	
	if (!$fn)
		{ $fn = $rv{n}{givenName}[0].' '.$rv{n}{familyName}[0]; }
	$rv{fn} = "$fn";

	# Nickname
	$rv{nickname} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#nickname',
		'http://www.w3.org/2006/vcard/ns#nickname',
		'http://www.w3.org/2001/vcard-rdf/3.0#NICKNAME',
		'http://xmlns.com/foaf/0.1/nick',
		'http://open.vocab.org/terms/familiarName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs'],
		['http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Sort-String
	my $sortstring = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#sort-string',
		'http://www.w3.org/2006/vcard/ns#sort-string',
		'http://www.w3.org/2001/vcard-rdf/3.0#SORT-STRING',
		'http://www.w3.org/2000/10/swap/pim/contact#sortName'],
		['http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'sort-string'} = $sortstring
		if ($sortstring);

	# Title
	$rv{title} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#title',
		'http://www.w3.org/2006/vcard/ns#title',
		'http://www.w3.org/2001/vcard-rdf/3.0#TITLE'],
		['http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Role
	$rv{role} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#role',
		'http://www.w3.org/2006/vcard/ns#role',
		'http://www.w3.org/2001/vcard-rdf/3.0#ROLE'],
		['http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Addresses
	# TODO: Imply "type" from homeAdr, workAdr.
	my @adrs = allOfObj($S, 'urn:ietf:rfc:2426#adr',
		'http://www.w3.org/2006/vcard/ns#adr',
		'http://www.w3.org/2006/vcard/ns#homeAdr',
		'http://www.w3.org/2006/vcard/ns#workAdr',
		'http://www.w3.org/2006/vcard/ns#unlabelledAdr',
		'http://www.w3.org/2001/vcard-rdf/3.0#ADR',
		'http://www.w3.org/2000/10/swap/pim/contact#address',
		'http://www.w3.org/2000/10/swap/pim/contact#vacationHome');
	foreach my $adr (@adrs)
	{
		my $adrSubject = findSubject($triples, $adr->toString);
		if ($adrSubject)
		{
			if (Swignition::Export::Address::is($adrSubject))
			{
				my %a = Swignition::Export::Address::_outJSON($triples, $adr->toString);
				push @{$rv{adr}}, \%a;
			}
		}
		else
		{
			my $a = $adr->toString;
			$a =~ s/(^\s+|\s+$)//g;
			$a =~ s/\s+/ /g;
			push @{$rv{label}}, $a if ($a);
		}
	}

	# Geo
	my @geo = allOfObj($S, 'urn:ietf:rfc:2426#geo',
		'http://www.w3.org/2006/vcard/ns#geo',
		'http://xmlns.com/foaf/0.1/based_near',
		'http://www.w3.org/2001/vcard-rdf/3.0#GEO');
	if ($geo[0])
	{
		my $geoSubject = findSubject($triples, $geo[0]->toString);
		if ($geoSubject)
		{
			my %G = Swignition::Export::Point::_outJSON($triples, $geoSubject->uri);
			$rv{geo} = \%G;
		}
	}

	# Taxonomy
	my @species = allOfObj($S,
		'http://purl.org/NET/biol/ns#hasTaxonomy',
		'urn:ietf:rfc:2426#x-taxonomy');
	foreach my $species (@species)
	{
		my $speciesSubject = findSubject($triples, $species->toString);
		if ($speciesSubject)
		{
			if (Swignition::Export::Taxonomy::is($speciesSubject))
			{
				my $val = Swignition::Export::Taxonomy::outText($triples, $speciesSubject->uri);
				push @{ $rv{'x-taxonomy'} }, $val;
			}
		}
		elsif (!$species->type)
		{
			push @{ $rv{'x-taxonomy'} }, $species->toString;
		}
	}


	# Labels - TODO: vcardx:usage!
	push @{$rv{label}}, allOfLiteral($S, 
		['urn:ietf:rfc:2426#label',
		'http://www.w3.org/2006/vcard/ns#label',
		'http://www.w3.org/2001/vcard-rdf/3.0#LABEL'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);

	# Telephone numbers
	my @tels = allOfObj($S, 'urn:ietf:rfc:2426#tel',
		'http://www.w3.org/2006/vcard/ns#tel',
		'http://www.w3.org/2006/vcard/ns#homeTel',
		'http://www.w3.org/2006/vcard/ns#workTel',
		'http://www.w3.org/2006/vcard/ns#mobileTel',
		'http://www.w3.org/2006/vcard/ns#unlabelledTel',
		'http://www.w3.org/2006/vcard/ns#fax',
		'http://www.w3.org/2001/vcard-rdf/3.0#TEL',
		'http://www.w3.org/2000/10/swap/pim/contact#phone',
		'http://www.w3.org/2000/10/swap/pim/contact#mobile',
		'http://xmlns.com/foaf/0.1/phone');
	foreach my $tel (@tels)
	{
		my $telSubject = findSubject($triples, $tel->toString);

		my $usages;
		$usages = 'CELL'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#mobileTel');
		$usages = 'CELL'
			if ($tel->property->uri eq 'http://www.w3.org/2000/10/swap/pim/contact#mobile');
		$usages = 'FAX'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#fax');
		$usages = 'HOME'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#homeTel');
		$usages = 'WORK'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#workTel');
			
		if ($telSubject)
		{
			my @usages = allOfLiteral($telSubject,
				['urn:ietf:rfc:2426#type',
				'http://buzzword.org.uk/rdf/vcardx#usage'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$usages = uc(join ',', @usages);
		}
		
		$usages = lc($usages);
		my @usages = split /\,/, $usages;
		
		my $telno = $tel->toString;
		if ($telno =~ /^tel\:/)
		{
			$telno =~ s/^tel\://;
			$telno =~ s/\;.*$//g;
		}
		else
		{
			$telno = firstOfLiteral($telSubject,
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
		}
		
		if (!$usages) { $usages = 'voice'; }

		my %tel;
		$tel{'value'} = $telno;
		$tel{'type'} = [@usages];
		push @{$rv{'tel'}}, \%tel;
	}

	# E-mail addresses. W3C vocab defines a bunch of weird e-mail types which
	# have nothing to do with vCard. This procedure is almost identical to "TEL:"
	# but I don't think we can wrap it all in a function.
	my @emails = allOfObj($S, 'urn:ietf:rfc:2426#email',
		'http://www.w3.org/2006/vcard/ns#email',
		'http://www.w3.org/2006/vcard/ns#workEmail',
		'http://www.w3.org/2006/vcard/ns#mobileEmail',
		'http://www.w3.org/2006/vcard/ns#unlabelledEmail',
		'http://www.w3.org/2001/vcard-rdf/3.0#EMAIL',
		'http://www.w3.org/2000/10/swap/pim/contact#emailAddress',
		'http://xmlns.com/foaf/0.1/mbox',
		'http://xmlns.com/foaf/0.1/mbox_sha1sum');
	foreach my $email (@emails)
	{
		my $emailSubject = findSubject($triples, $email->toString);

		my $usages;
		if ($emailSubject)
		{
			my @usages = allOfLiteral($emailSubject, 
				['urn:ietf:rfc:2426#type',
				'http://buzzword.org.uk/rdf/vcardx#usage'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$usages = uc(join ',', @usages);
		}
		
		my $emailno = $email->toString;
		$usages = 'X-BRUCE-SCHNEIER'
			if ($email->property->uri eq 'http://xmlns.com/foaf/0.1/mbox_sha1sum');
		if ($emailno =~ /^mailto\:/)
		{
			$emailno =~ s/^mailto\://;
			$emailno =~ s/\?.*$//g;
		}
		else
		{
			my $emailno2 = firstOfLiteral($emailSubject,
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$emailno = $emailno2 if ($emailno2);
		}
		
		if (!$usages) { $usages = 'internet'; }
		
		my @usages = split /\,/, lc($usages);
		my %tel;
		$tel{'value'} = $emailno;
		$tel{'type'} = [@usages];
		push @{$rv{'email'}}, \%tel;
	}

	# URLs - Note: W3C's 2001 vCard RDF vocab does not contain URL!
	my @urls = allOf($S, 'urn:ietf:rfc:2426#url',
		'http://www.w3.org/2006/vcard/ns#url',
		'http://xmlns.com/foaf/0.1/homepage',
		'http://xmlns.com/foaf/0.1/weblog',
		'http://xmlns.com/foaf/0.1/workplaceHomepage',
		'http://xmlns.com/foaf/0.1/workInfoHomepage',
		'http://xmlns.com/foaf/0.1/schoolHomepage',
		'http://xmlns.com/foaf/0.1/page',
		'http://xmlns.com/foaf/0.1/tipjar',
		'http://www.w3.org/2000/10/swap/pim/contact#homepage',
		'http://www.w3.org/2000/10/swap/pim/contact#publicHomepage');
	foreach my $url (@urls)
	{
		push @{$rv{'url'}}, $url;
		if ($url =~ /^(sip|xmpp|irc|ymsgr|msn|aim|im|pres|msnim|skype)\:/i)
			{ push @{$rv{'impp'}}, { value => $url }; }
	}

	# IMPP
	my @impps = allOfObj($S,
		'urn:ietf:rfc:4770#impp',
		'http://buzzword.org.uk/rdf/vcardx#impp');
	foreach my $impp (@impps)
	{
		my ($usages, @usages);
		
		my $imppSubject = findSubject($triples, $impp->toString);
		if ($imppSubject)
		{
			@usages = allOf($imppSubject,
				'urn:ietf:rfc:4770#type',
				'http://buzzword.org.uk/rdf/vcardx#usage');
			$usages = uc(join ',', @usages);
			@usages = split /\,/, lc($usages);
		
			my $i = firstOf($imppSubject,
				'urn:ietf:rfc:4770#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value');
			my %i;
			if (length $i)
			{
				$i{'value'} = "$i";
				$i{'type'} = [@usages];
				push @{$rv{'impp'}}, \%i;
			}
		}
		else
		{
			my %i;
			$i{'value'} = $impp->toString;
			push @{$rv{'impp'}}, \%i;
		}
	}
	
	my %imppMap = (
		'http://xmlns.com/foaf/0.1/aimChatID'    => 'aim:goim?screenname=%s',
		'http://xmlns.com/foaf/0.1/icqChatID'    => 'http://www.icq.com/people/cmd.php?uin=%d&action=message',
		'http://xmlns.com/foaf/0.1/jabberID'     => 'xmpp:%s',
		'http://xmlns.com/foaf/0.1/msnChatID'    => 'msnim:chat?contact=%s',
		'http://xmlns.com/foaf/0.1/yahooChatID'  => 'ymsgr:sendIM?%s',
		'http://open.vocab.org/terms/skypeID'    => 'skype:'
	);
	foreach my $pred (keys %imppMap)
	{
		my @impps = allOf($S, $pred);
		foreach my $impp (@impps)
		{
			push @{$rv{'impp'}}, { value => sprintf($imppMap{$pred}, $impp) };
		}
	}
	
	# Org
	my @orgs = allOfObj($S, 'urn:ietf:rfc:2426#org',
		'http://www.w3.org/2006/vcard/ns#org',
		'http://www.w3.org/2001/vcard-rdf/3.0#ORG');
	foreach my $org (@orgs)
	{
		my $orgSubject = findSubject($triples, $org->toString);
		
		if ($orgSubject)
		{
			my %o;
		
			my $orgstring = firstOfLiteral($orgSubject,
				['urn:ietf:rfc:2426#organization-name',
				'http://www.w3.org/2006/vcard/ns#organization-name',
				'http://www.w3.org/2001/vcard-rdf/3.0#Orgname'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$o{'organization-name'} = "$orgstring";

			my @units = allOfLiteral($orgSubject,
				['urn:ietf:rfc:2426#organization-unit',
				'http://www.w3.org/2006/vcard/ns#organization-unit',
				'http://www.w3.org/2001/vcard-rdf/3.0#Orgunit'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$o{'organization-unit'} = [@units] if (@units);

			push @{$rv{'org'}}, \%o;
		}
		else
		{
			push @{$rv{'org'}}, $org->toString;
		}
	}

	# Mailer
	$rv{'mailer'} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#mailer',
		'http://www.w3.org/2006/vcard/ns#mailer',
		'http://www.w3.org/2001/vcard-rdf/3.0#MAILER'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Photo
	$rv{'photo'} = [allOf($S, 'urn:ietf:rfc:2426#photo',
		'http://www.w3.org/2006/vcard/ns#photo',
		'http://www.w3.org/2001/vcard-rdf/3.0#PHOTO',
		'http://xmlns.com/foaf/0.1/img',
		'http://xmlns.com/foaf/0.1/depiction')];

	# Logo
	$rv{'logo'} = [allOf($S, 'urn:ietf:rfc:2426#logo',
		'http://www.w3.org/2006/vcard/ns#logo',
		'http://www.w3.org/2001/vcard-rdf/3.0#LOGO',
		'http://xmlns.com/foaf/0.1/logo')];

	# Sound
	$rv{'sound'} = [allOf($S, 'urn:ietf:rfc:2426#sound',
		'http://www.w3.org/2006/vcard/ns#sound',
		'http://www.w3.org/2001/vcard-rdf/3.0#SOUND')];

	# Key (crypto)
	$rv{'key'} = [allOf($S, 'urn:ietf:rfc:2426#key',
		'http://www.w3.org/2006/vcard/ns#key',
		'http://www.w3.org/2001/vcard-rdf/3.0#KEY')];
	
	# Keys (WOT RDF schema)
	my @keys = allOfObj($S, 'http://xmlns.com/wot/0.1/hasKey');
	foreach my $key (@keys)
	{
		my $keySubject = findSubject($triples, $key->toString);
		if ($keySubject)
		{
			my $uri = firstOf($keySubject, 'http://xmlns.com/wot/0.1/pubkeyAddress');
			push @{$rv{'key'}}, $uri
				if ($uri);
		}
	}

	# Notes
	$rv{'note'} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#note',
		'http://www.w3.org/2006/vcard/ns#note',
		'http://www.w3.org/2001/vcard-rdf/3.0#NOTE',
		'http://www.w3.org/2000/01/rdf-schema#comment',
		'http://purl.org/dc/terms/abstract',
		'http://purl.org/dc/terms/description',
		'http://purl.org/dc/elements/1.1/description',
		'http://xmlns.com/foaf/0.1/plan',
		'http://xmlns.com/foaf/0.1/geekcode'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

 	# Agents
 	my @agents = allOfObj($S, 'urn:ietf:rfc:2426#agent',
 		'http://www.w3.org/2001/vcard-rdf/3.0#AGENT',
 		'http://www.w3.org/2006/vcard/ns#agent');
 	foreach my $agent (@agents)
	{
		my $agentSubject = findSubject($triples, $agent->toString);
		if ($agentSubject)
		{
			if (is($agentSubject))
			{
				my %a = _outJSON($triples, $agentSubject->uri);
				push @{$rv{'agent'}}, \%a;
			}
			else
			{
				my $a = firstOfLiteral($agentSubject,
					['urn:ietf:rfc:2426#fn',
					'http://www.w3.org/2006/vcard/ns#fn',
					'http://www.w3.org/2001/vcard-rdf/3.0#FN',
					'http://xmlns.com/foaf/0.1/fullName',
					'http://xmlns.com/foaf/0.1/name',
					'http://open.vocab.org/terms/fullName',
					'http://open.vocab.org/terms/informalName',
					'http://open.vocab.org/terms/formalName',
					'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
					'http://purl.org/dc/terms/title',
					'http://purl.org/dc/elements/1.1/title'],
					['urn:ietf:rfc:2426#value',
					'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
				push @{$rv{'agent'}}, $a if ($a);
			}
		}
		else
		{
			my $a = $agent->toString;
			$a =~ s/(^\s+|\s+$)//g;
			$a =~ s/\s+/ /g;
			push @{$rv{'agent'}}, $a if ($a);
		}
	}

 	# Members
 	my @agents = allOfObj($S,
 		'urn:ietf:rfc:2426#member',
 		'http://xmlns.com/foaf/0.1/member',
 		'http://buzzword.org.uk/rdf/vcardx#member');
 	foreach my $agent (@agents)
	{
		my $agentSubject = findSubject($triples, $agent->toString);
		if ($agentSubject)
		{
			if (is($agentSubject))
			{
				my %a = _outJSON($triples, $agentSubject->uri);
				push @{$rv{'member'}}, \%a;
			}
			else
			{
				push @{$rv{'member'}}, $agent->toString;
			}
		}
		else
		{
			my $a = $agent->toString;
			$a =~ s/(^\s+|\s+$)//g;
			$a =~ s/\s+/ /g;
			push @{$rv{'member'}}, $a if ($a);
		}
	}

	# TZ (timezone)
	my $tz = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#tz',
		'http://www.w3.org/2006/vcard/ns#tz',
		'http://www.w3.org/2001/vcard-rdf/3.0#TZ'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'tz'} = $tz if ($tz);

	# Bday (date of birth)
	my $bday = firstOfLiteral($S,
		['urn:ietf:rfc:2426#bday',
		'http://www.w3.org/2006/vcard/ns#bday',
		'http://www.w3.org/2001/vcard-rdf/3.0#BDAY',
		'http://www.w3.org/2000/10/swap/pim/contact#birthday',
		'http://xmlns.com/foaf/0.1/birthday'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'bday'} = "$bday" if ($bday);

	# Dday (date of birth)
	my $dday = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#dday',
		'http://buzzword.org.uk/rdf/vcardx#dday',
		'http://open.vocab.org/terms/dateOfDeath'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'dday'} = "$dday" if ($dday);
		
	# Gender
	my $gender = firstOfLiteral($S,
		['urn:ietf:rfc:2426#gender',
		'http://buzzword.org.uk/rdf/vcardx#gender',
		'http://xmlns.com/foaf/0.1/gender'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	if ($gender)
	{
		$rv{'gender'} = $gender;
	}
	else
	{
		if (inProp($S, 'http://www.w3.org/2000/10/swap/pim/contact#Male',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))
		{
			$rv{'gender'} = 'Male';
		}

		if (inProp($S, 'http://www.w3.org/2000/10/swap/pim/contact#Female',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))
		{
			$rv{'gender'} = 'Female';
		}
	}
	
	# Categories
	my @cats = allOfObj($S, 'urn:ietf:rfc:2426#category',
		'http://www.w3.org/2006/vcard/ns#category',
		'http://www.w3.org/2001/vcard-rdf/3.0#CATEGORIES',
		'http://www.holygoat.co.uk/owl/redwood/0.1/tags/taggedWithTag');
	my $catstring = '';
	foreach my $cat (@cats)
	{
		my $catSubject = findSubject($triples, $cat->toString);
		if ($catSubject)
		{
			my $category = firstOf($catSubject,
				'http://www.holygoat.co.uk/owl/redwood/0.1/tags/name',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value',
				'http://purl.org/dc/terms/description',
				'http://purl.org/dc/elements/1.1/description');
			$catstring .= "|@@|$category"
				if (length $category);
		}
		else
		{
			my $category = $cat->toString;
			$category =~ s/(^\s+|\s+$)//g;
			$category =~ s/\s+/ /g;
			$catstring .= "|@@|$category";
		}
	}
	$catstring =~ s/^\|\@\@\|//;
	if ($catstring)
	{
		$rv{'categories'} =  $catstring;
		$rv{'categories'} =~ s/\|\@\@\|/\,/g;
		$rv{'category'}   =  [split /\|\@\@\|/, $catstring];
	}

	# Class
	my $class = uc(firstOfLiteral($S, 
		['urn:ietf:rfc:2426#class',
		'http://www.w3.org/2006/vcard/ns#class',
		'http://www.w3.org/2001/vcard-rdf/3.0#CLASS'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']));
	$class =~ s/\s//g;
	$rv{'class'} = lc($class)
		if ($class =~ /^(PUBLIC|PRIVATE|CONFIDENTIAL|X-)/);
		
	# Rev
	$rv{'rev'} = [allOfLiteral($S,
		['urn:ietf:rfc:2426#rev',
		'http://www.w3.org/2006/vcard/ns#rev',
		'http://www.w3.org/2001/vcard-rdf/3.0#REV',
		'http://purl.org/dc/terms/modified',
		'http://purl.org/dc/elements/1.1/modified'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Birth - TODO: accept vCard/adr/geo?
	my $birth = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#birth',
		'http://buzzword.org.uk/rdf/vcardx#birth'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'birth'} = $birth
		if ($birth);

	# Death - TODO: accept vCard/adr/geo?
	my $death = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#death',
		'http://buzzword.org.uk/rdf/vcardx#death'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'death'} = $death
		if ($death);

	# Lang
	# TODO: there are a bunch of schemas out there for speaks, writes & reads.
	$rv{'lang'} = [allOfLiteral($S, 
		['urn:ietf:rfc:2426#lang',
		'http://buzzword.org.uk/rdf/vcardx#lang',
		'http://www.w3.org/2000/10/swap/pim/contact#motherTongue'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];

	# Kind
	my $kind = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#kind',
		'http://buzzword.org.uk/rdf/vcardx#kind'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	$rv{'kind'} = $kind
		if ($kind);

	# fburl / caluri / caladruri
	foreach my $p (qw(fburl caluri caladruri))
	{
		my @predicates = ('urn:ietf:rfc:2426#'.$p,
			'http://buzzword.org.uk/rdf/vcardx#'.$p,
			'http://www.w3.org/2006/vcard/ns#'.$p,
			'http://www.w3.org/2001/vcard-rdf/3.0#'.uc($p));
		push @predicates, 'http://open.vocab.org/terms/calendar'
			if ($p eq 'caluri');
			
		my @vals = allOf($S, @predicates);
		foreach my $v (@vals)
		{
			if ($v !~ /^bnode\:/)
			{
				push @{$rv{$p}}, $v;
				next;
			}
	
			my $V = findSubject($triples, $v);
			if (Swignition::Export::Calendar::is($V))
			{
				my $calData = Swignition::Export::Calendar::outICS($triples, $V->uri);
				push @{$rv{$p}}, 'data:text/calendar;base64,'.encode_base64($calData);
			}
		}
	}

	# Related
	my $rel;
	my @relationships = ('http://gmpg.org/xfn/11#contact',
		'http://gmpg.org/xfn/11#friend',
		'http://gmpg.org/xfn/11#acquaintance',
		'http://gmpg.org/xfn/11#met',
		'http://gmpg.org/xfn/11#co-worker',
		'http://gmpg.org/xfn/11#colleague',
		'http://gmpg.org/xfn/11#co-resident',
		'http://gmpg.org/xfn/11#neighbor',
		'http://gmpg.org/xfn/11#child',
		'http://gmpg.org/xfn/11#parent',
		'http://gmpg.org/xfn/11#sibling',
		'http://gmpg.org/xfn/11#spouse',
		'http://gmpg.org/xfn/11#kin',
		'http://gmpg.org/xfn/11#muse',
		'http://gmpg.org/xfn/11#crush',
		'http://gmpg.org/xfn/11#date',
		'http://gmpg.org/xfn/11#sweetheart',
		'http://vocab.sindice.com/xfn#contact',
		'http://vocab.sindice.com/xfn#friend',
		'http://vocab.sindice.com/xfn#acquaintance',
		'http://vocab.sindice.com/xfn#met',
		'http://vocab.sindice.com/xfn#co-worker',
		'http://vocab.sindice.com/xfn#colleague',
		'http://vocab.sindice.com/xfn#co-resident',
		'http://vocab.sindice.com/xfn#neighbor',
		'http://vocab.sindice.com/xfn#child',
		'http://vocab.sindice.com/xfn#parent',
		'http://vocab.sindice.com/xfn#sibling',
		'http://vocab.sindice.com/xfn#spouse',
		'http://vocab.sindice.com/xfn#kin',
		'http://vocab.sindice.com/xfn#muse',
		'http://vocab.sindice.com/xfn#crush',
		'http://vocab.sindice.com/xfn#date',
		'http://vocab.sindice.com/xfn#sweetheart',
		'http://xen.adactio.com/#nemesis',
		'http://xen.adactio.com/#enemy',
		'http://xen.adactio.com/#nuisance',
		'http://xen.adactio.com/#evil-twin',
		'http://xen.adactio.com/#rival',
		'http://xen.adactio.com/#fury',
		'http://xen.adactio.com/#creep',
		'http://buzzword.org.uk/rdf/xen#nemesis',
		'http://buzzword.org.uk/rdf/xen#enemy',
		'http://buzzword.org.uk/rdf/xen#nuisance',
		'http://buzzword.org.uk/rdf/xen#evil-twin',
		'http://buzzword.org.uk/rdf/xen#rival',
		'http://buzzword.org.uk/rdf/xen#fury',
		'http://buzzword.org.uk/rdf/xen#creep',
		'http://xmlns.com/foaf/0.1/knows',
		'http://purl.org/vocab/relationship/acquaintanceOf',
		'http://purl.org/vocab/relationship/ambivalentOf',
		'http://purl.org/vocab/relationship/ancestorOf',
		'http://purl.org/vocab/relationship/antagonistOf',
		'http://purl.org/vocab/relationship/apprenticeTo',
		'http://purl.org/vocab/relationship/childOf',
		'http://purl.org/vocab/relationship/closeFriendOf',
		'http://purl.org/vocab/relationship/collaboratesWith',
		'http://purl.org/vocab/relationship/colleagueOf',
		'http://purl.org/vocab/relationship/descendantOf',
		'http://purl.org/vocab/relationship/employedBy',
		'http://purl.org/vocab/relationship/employerOf',
		'http://purl.org/vocab/relationship/enemyOf',
		'http://purl.org/vocab/relationship/engagedTo',
		'http://purl.org/vocab/relationship/friendOf',
		'http://purl.org/vocab/relationship/grandchildOf',
		'http://purl.org/vocab/relationship/grandparentOf',
		'http://purl.org/vocab/relationship/hasMet',
		'http://purl.org/vocab/relationship/knowsByReputation',
		'http://purl.org/vocab/relationship/knowsInPassing',
		'http://purl.org/vocab/relationship/knowsOf',
		'http://purl.org/vocab/relationship/lifePartnerOf',
		'http://purl.org/vocab/relationship/livesWith',
		'http://purl.org/vocab/relationship/lostContactWith',
		'http://purl.org/vocab/relationship/mentorOf',
		'http://purl.org/vocab/relationship/neighborOf',
		'http://purl.org/vocab/relationship/parentOf',
		'http://purl.org/vocab/relationship/siblingOf',
		'http://purl.org/vocab/relationship/spouseOf',
		'http://purl.org/vocab/relationship/worksWith',
		'http://www.w3.org/2000/10/swap/pim/contact#partner',
		'http://open.vocab.org/terms/hearts');
	foreach my $relationship (@relationships)
	{
		my @related = allOfObj($S, $relationship);
		foreach my $r (@related)
		{
			my $uri;
			
			if ($r->{type})
			{
				my $R = findSubject($triples, $r->toString);
				if ($R)
				{
					my $uid = firstOf($R, 'urn:ietf:rfc:2426#uid',
						'http://www.w3.org/2006/vcard/ns#uid',
						'http://www.w3.org/2001/vcard-rdf/3.0#UID');
					if ($uid)
					{
						$uri = $uid;
						$rel->{$uri}->{VALUE} = ($uid =~ /^[a-z0-9]+\:/i)?'uri':'text';
					}
					else
					{
						my $fn = firstOfLiteral($R,
							['urn:ietf:rfc:2426#fn',
							'http://www.w3.org/2006/vcard/ns#fn',
							'http://www.w3.org/2001/vcard-rdf/3.0#FN',
							'http://xmlns.com/foaf/0.1/fullName',
							'http://xmlns.com/foaf/0.1/name',
							'http://open.vocab.org/terms/fullName',
							'http://open.vocab.org/terms/informalName',
							'http://open.vocab.org/terms/formalName',
							'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
							'http://purl.org/dc/terms/title',
							'http://purl.org/dc/elements/1.1/title'],
							['urn:ietf:rfc:2426#value',
							'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);	
						if ($fn)
						{
							$uri = $fn;
							$rel->{$uri}->{VALUE} = 'text';
						}
						else
						{
							$uri = $r->toString;
							$rel->{$uri}->{VALUE} = 'uri';
						}
					}
				}
				else
				{
					$uri = $r->toString;
					$rel->{$uri}->{VALUE} = 'uri';
				}
			}
			else
			{
				$uri = $r->toString;
				$rel->{$uri}->{VALUE} = 'text';
			}
			
			my $prop = $r->property->localname;
			
			if ($prop =~ /^(parentOf|child)$/)
				{ $rel->{$uri}->{TYPE}->{child} = 1; }
			elsif ($prop =~ /^(childOf|parent)$/)
				{ $rel->{$uri}->{TYPE}->{parent} = 1; }
			elsif ($prop =~ /^(siblingOf|sibling)$/)
				{ $rel->{$uri}->{TYPE}->{sibling} = 1; }
			elsif ($prop =~ /^(apprenticeTo|employedBy)$/)
				{ $rel->{$uri}->{TYPE}->{manager} = 1; }
			elsif ($prop =~ /^(employerOf|mentorOf)$/)
				{ $rel->{$uri}->{TYPE}->{assistant} = 1; }
			else
				{ $rel->{$uri}->{TYPE}->{'x-'.$prop} = 1; }			
	
		}
	}
	foreach my $uri (keys %$rel)
	{
		my @types = keys %{$rel->{$uri}->{TYPE}};
		push @{$rv{'related'}}, {
			type  => \@types,
			value => $uri,
			value_type => $rel->{$uri}->{VALUE}
		};
	}


	# Remove empty leafnodes from structure.	
	foreach my $k (keys %rv)
	{
		if (!defined $rv{$k})
		{
			delete $rv{$k};
		}
		elsif ((ref $rv{$k}) eq 'ARRAY')
		{
			delete $rv{$k} unless scalar(@{$rv{$k}});
		}
	}

	# Remove empty leafnodes from name parts.	
	my $hasN = 0;
	foreach my $k (keys %{$rv{'n'}})
	{
		if ($k eq 'value')
			{ $hasN++ if (length $rv{'n'}{$k}); }
		elsif (scalar(@{$rv{'n'}{$k}}))
			{ $hasN++; }
		else 
			{ delete $rv{'n'}{$k}; }
	}
	unless ($hasN)
		{ delete $rv{'n'}; }
	
	return %rv;
}

sub outVCF
{
	my $page    = shift;
	my $graph   = shift;
	my $subject = shift;
	
	my $subject_node = $graph->lookup_node($subject);
	my $S = [$page, $graph, $subject_node];
	my @rv = (['BEGIN','VCARD']);

	my $triples = undef;

	# UID
	my $uid = firstOf($S, 'urn:ietf:rfc:2426#uid',
		'http://www.w3.org/2006/vcard/ns#uid',
		'http://www.w3.org/2001/vcard-rdf/3.0#UID',
		'http://purl.org/dc/terms/identifier',
		'http://purl.org/dc/elements/1.1/identifier');
	push @rv, ['UID', $uid]
		if ($uid);
	
	# Formatted name
	my $fn = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#fn',
		'http://www.w3.org/2006/vcard/ns#fn',
		'http://www.w3.org/2001/vcard-rdf/3.0#FN',
		'http://xmlns.com/foaf/0.1/fullName',
		'http://xmlns.com/foaf/0.1/name',
		'http://open.vocab.org/terms/fullName',
		'http://open.vocab.org/terms/informalName',
		'http://open.vocab.org/terms/formalName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
		'http://purl.org/dc/terms/title',
		'http://purl.org/dc/elements/1.1/title',
		'http://www.w3.org/2000/01/rdf-schema#label'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);	
	if (!$fn)
		{ $fn = outName($triples, $subject); }
	push @rv, ['FN',$fn];

	# Structured name
	my $n = outN($triples, $subject);
	push @rv, ['N',$n,1] if (length $n);
	
	# Nickname
	my @nicknames = allOfLiteral($S, 
		['urn:ietf:rfc:2426#nickname',
		'http://www.w3.org/2006/vcard/ns#nickname',
		'http://www.w3.org/2001/vcard-rdf/3.0#NICKNAME',
		'http://xmlns.com/foaf/0.1/nick',
		'http://open.vocab.org/terms/familiarName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $nickname (@nicknames)
		{ push @rv, ['NICKNAME',$nickname]; }

	# Sort-String
	my $sortstring = firstOfLiteral($S, 
		['urn:ietf:rfc:2426#sort-string',
		'http://www.w3.org/2006/vcard/ns#sort-string',
		'http://www.w3.org/2001/vcard-rdf/3.0#sort-string',
		'http://www.w3.org/2000/10/swap/pim/contact#sortName'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['SORT-STRING',$sortstring]
		if ($sortstring);

	# Title
	my @titles = allOfLiteral($S,
		['urn:ietf:rfc:2426#title',
		'http://www.w3.org/2006/vcard/ns#title',
		'http://www.w3.org/2001/vcard-rdf/3.0#TITLE'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $title (@titles)
		{ push @rv, ['TITLE',$title]; }

	# Role
	my @roles = allOfLiteral($S,
		['urn:ietf:rfc:2426#role',
		'http://www.w3.org/2006/vcard/ns#role',
		'http://www.w3.org/2001/vcard-rdf/3.0#ROLE'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $role (@roles)
		{ push @rv, ['ROLE',$role]; }

	# Addresses
	# TODO: homeAdr, workAdr imply types.
	my @adrs = allOfObj($S, 'urn:ietf:rfc:2426#adr',
		'http://www.w3.org/2006/vcard/ns#adr',
		'http://www.w3.org/2006/vcard/ns#homeAdr',
		'http://www.w3.org/2006/vcard/ns#workAdr',
		'http://www.w3.org/2006/vcard/ns#unlabelledAdr',
		'http://www.w3.org/2001/vcard-rdf/3.0#ADR',
		'http://www.w3.org/2000/10/swap/pim/contact#address',
		'http://www.w3.org/2000/10/swap/pim/contact#vacationHome');
	foreach my $adr (@adrs)
	{
		my $adrSubject = findSubject($triples, $adr->toString);
		if ($adrSubject)
		{
			if (Swignition::Export::Address::is($adrSubject))
			{
				my ($key, $val) = Swignition::Export::Address::outVCFLine($triples, $adrSubject->uri);
				push @rv, [$key, $val, 1];
			}
		}
		elsif ($adr->type)
		{
			push @rv, ['LABEL;VALUE=uri',$adr->toString];
		}
		else
		{
			my $a = $adr->toString;
			$a =~ s/(^\s+|\s+$)//g;
			$a =~ s/\s+/ /g;
			push @rv, ['LABEL',$a] if ($a);
		}
	}

	# Labels - TODO: usages
	my @labels = allOfLiteral($S,
		['urn:ietf:rfc:2426#label',
		'http://www.w3.org/2006/vcard/ns#label',
		'http://www.w3.org/2001/vcard-rdf/3.0#LABEL'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $label (@labels)
		{ push @rv, ['LABEL',$label]; }

	# Geo
	my @geos = allOfObj($S, 'urn:ietf:rfc:2426#geo',
		'http://www.w3.org/2006/vcard/ns#geo',
		'http://xmlns.com/foaf/0.1/based_near',
		'http://www.w3.org/2001/vcard-rdf/3.0#GEO');
	foreach my $geo (@geos)
	{
		my $geoSubject = findSubject($triples, $geo->toString);
		if ($geoSubject)
		{
			if (Swignition::Export::Point::is($geoSubject))
			{
				my ($key, $val) = Swignition::Export::Point::outVCFLine($triples, $geoSubject->uri);
				push @rv, [$key, $val, 1];
			}
		}
		elsif (!$geo->type)
		{
			push @rv, ['GEO', $geo->toString, 1];
		}
	}

	# Taxonomy
	my @species = allOfObj($S,
		'http://purl.org/NET/biol/ns#hasTaxonomy',
		'urn:ietf:rfc:2426#x-taxonomy');
	foreach my $species (@species)
	{
		my $speciesSubject = findSubject($triples, $species->toString);
		if ($speciesSubject)
		{
			if (Swignition::Export::Taxonomy::is($speciesSubject))
			{
				my $val = Swignition::Export::Taxonomy::outText($triples, $speciesSubject->uri);
				push @rv, ['X-TAXONOMY', $val];
			}
		}
		elsif (!$species->type)
		{
			push @rv, ['X-TAXONOMY', $species->toString, 1];
		}
	}

	# Telephone numbers
	my @tels = allOfObj($S, 'urn:ietf:rfc:2426#tel',
		'http://www.w3.org/2006/vcard/ns#tel',
		'http://www.w3.org/2006/vcard/ns#homeTel',
		'http://www.w3.org/2006/vcard/ns#workTel',
		'http://www.w3.org/2006/vcard/ns#mobileTel',
		'http://www.w3.org/2006/vcard/ns#unlabelledTel',
		'http://www.w3.org/2006/vcard/ns#fax',
		'http://www.w3.org/2001/vcard-rdf/3.0#TEL',
		'http://www.w3.org/2000/10/swap/pim/contact#phone',
		'http://www.w3.org/2000/10/swap/pim/contact#mobile',
		'http://xmlns.com/foaf/0.1/phone');
	foreach my $tel (@tels)
	{
		my $telSubject = findSubject($triples, $tel->toString);

		my $usages;
		$usages = 'CELL'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#mobileTel');
		$usages = 'CELL'
			if ($tel->property->uri eq 'http://www.w3.org/2000/10/swap/pim/contact#mobile');
		$usages = 'FAX'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#fax');
		$usages = 'HOME'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#homeTel');
		$usages = 'WORK'
			if ($tel->property->uri eq 'http://www.w3.org/2006/vcard/ns#workTel');
			
		if ($telSubject)
		{
			my @usages = allOfLiteral($telSubject,
				['urn:ietf:rfc:2426#type',
				'http://buzzword.org.uk/rdf/vcardx#usage'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$usages = uc(join ',', @usages);
		}
		
		my $telno = $tel->toString;
		if ($telno =~ /^tel\:/)
		{
			$telno =~ s/^tel\://;
			$telno =~ s/\;.*$//g;
		}
		else
		{
			$telno = firstOf($telSubject,
				'urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value');
		}
		
		if ($usages)
			{ push @rv, ["TEL;TYPE=$usages",$telno]; }
		else
			{ push @rv, ['TEL', $telno]; }
	}

	# E-mail addresses. W3C vocab defines a bunch of weird e-mail types which
	# have nothing to do with vCard. This procedure is almost identical to "TEL:"
	# but I don't think we can wrap it all in a function.
	my @emails = allOfObj($S, 'urn:ietf:rfc:2426#email',
		'http://www.w3.org/2006/vcard/ns#email',
		'http://www.w3.org/2006/vcard/ns#workEmail',
		'http://www.w3.org/2006/vcard/ns#mobileEmail',
		'http://www.w3.org/2006/vcard/ns#unlabelledEmail',
		'http://www.w3.org/2001/vcard-rdf/3.0#EMAIL',
		'http://www.w3.org/2000/10/swap/pim/contact#emailAddress',
		'http://xmlns.com/foaf/0.1/mbox',
		'http://xmlns.com/foaf/0.1/mbox_sha1sum');
	foreach my $email (@emails)
	{
		my $emailSubject = findSubject($triples, $email->toString);

		my $usages;
		if ($emailSubject)
		{
			my @usages = allOfLiteral($emailSubject,
				['urn:ietf:rfc:2426#type',
				'http://buzzword.org.uk/rdf/vcardx#usage'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$usages = uc(join ',', @usages);
		}
		
		my $emailno = $email->toString;
		$usages = 'X-BRUCE-SCHNEIER'
			if ($email->property->uri eq 'http://xmlns.com/foaf/0.1/mbox_sha1sum');
		if ($emailno =~ /^mailto\:/)
		{
			$emailno =~ s/^mailto\://;
			$emailno =~ s/\?.*$//g;
		}
		else
		{
			my $emailno2 = firstOf($emailSubject,
				'urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value');
			$emailno = $emailno2 if ($emailno2);
		}
		
		if ($usages)
			{ push @rv, ["EMAIL;TYPE=$usages",$emailno]; }
		else
			{ push @rv, ['EMAIL',$emailno]; }
	}

	# URLs
	my @urls = allOf($S, 'urn:ietf:rfc:2426#url',
		'http://www.w3.org/2006/vcard/ns#url',
		'http://xmlns.com/foaf/0.1/homepage',
		'http://xmlns.com/foaf/0.1/weblog',
		'http://xmlns.com/foaf/0.1/workplaceHomepage',
		'http://xmlns.com/foaf/0.1/workInfoHomepage',
		'http://xmlns.com/foaf/0.1/schoolHomepage',
		'http://xmlns.com/foaf/0.1/page',
		'http://xmlns.com/foaf/0.1/tipjar',
		'http://www.w3.org/2000/10/swap/pim/contact#homepage',
		'http://www.w3.org/2000/10/swap/pim/contact#publicHomepage');
	foreach my $url (@urls)
	{
		push @rv, ['URL', $url];
		push @rv, ['IMPP', $url]
			if ($url =~ /^(sip|xmpp|irc|ymsgr|msn|aim|im|pres|msnim|skype)\:/i);
	}

	# IMPP
	my @impps = allOfObj($S,
		'urn:ietf:rfc:4770#impp',
		'http://buzzword.org.uk/rdf/vcardx#impp');
	foreach my $impp (@impps)
	{
		my $usages;
		
		my $imppSubject = findSubject($triples, $impp);
		if ($imppSubject)
		{
			my @usages = allOfLiteral($imppSubject,
				['urn:ietf:rfc:4770#type',
				'http://buzzword.org.uk/rdf/vcardx#usage'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$usages = uc(join ',', @usages);
			my $i = firstOf($imppSubject,
				'urn:ietf:rfc:4770#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value');
			if (length $i)
			{
				if (length $usages)
					{ push @rv, ['IMPP;TYPE='.$usages, $i]; }
				else
					{ push @rv, ['IMPP', $i]; }
			}
		}
		
		else
		{
			push @rv, ['IMPP', $impp->toString];
		}
	}
	
	my %imppMap = (
		'http://xmlns.com/foaf/0.1/aimChatID'   => 'aim:goim?screenname=%s',
		'http://xmlns.com/foaf/0.1/icqChatID'   => 'http://www.icq.com/people/cmd.php?uin=%d&action=message',
		'http://xmlns.com/foaf/0.1/jabberID'    => 'xmpp:%s',
		'http://xmlns.com/foaf/0.1/msnChatID'   => 'msnim:chat?contact=%s',
		'http://xmlns.com/foaf/0.1/yahooChatID' => 'ymsgr:sendIM?%s',
		'http://open.vocab.org/terms/skypeID'   => 'skype:'
	);
	foreach my $pred (keys %imppMap)
	{
		my @impps = allOf($S, $pred);
		foreach my $impp (@impps)
		{
			push @rv, ['IMPP', sprintf($imppMap{$pred}, $impp)];
		}
	}

	# Org
	my @orgs = allOfObj($S, 'urn:ietf:rfc:2426#org',
		'http://www.w3.org/2006/vcard/ns#org',
		'http://www.w3.org/2001/vcard-rdf/3.0#ORG');
	foreach my $org (@orgs)
	{
		my ($orgSubject, $orgstring);
		$orgSubject = findSubject($triples, $org->toString);
		
		if ($orgSubject)
		{
			$orgstring = firstOfLiteral($orgSubject,
				['urn:ietf:rfc:2426#organization-name',
				'http://www.w3.org/2006/vcard/ns#organization-name',
				'http://www.w3.org/2001/vcard-rdf/3.0#Orgname'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			$orgstring =~ s/\;/\\\;/g;
			$orgstring =~ s/\,/\\\,/g;

			my @units = allOfLiteral($orgSubject,
				['urn:ietf:rfc:2426#organization-unit',
				'http://www.w3.org/2006/vcard/ns#organization-unit',
				'http://www.w3.org/2001/vcard-rdf/3.0#Orgunit'],
				['urn:ietf:rfc:2426#value',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
			foreach my $ou (@units)
			{
				$ou =~ s/\;/\\\;/g;
				$ou =~ s/\,/\\\,/g;
				$orgstring .= "; $ou";
			}
		}
		
		$orgstring = $org->toString
			unless (length $orgstring);

		if ($orgstring)
		{
			push @rv, ['ORG', $orgstring, ($orgSubject?1:0)];
		}
	}

	# Mailer
	my @mailers = allOfLiteral($S,
		['urn:ietf:rfc:2426#mailer',
		'http://www.w3.org/2006/vcard/ns#mailer',
		'http://www.w3.org/2001/vcard-rdf/3.0#MAILER'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $mailer (@mailers)
		{ push @rv, ['MAILER',$mailer]; }

	# fburl / caluri / caladruri
	foreach my $p (qw(fburl caluri caladruri))
	{
		my @predicates = ('urn:ietf:rfc:2426#'.$p,
			'http://buzzword.org.uk/rdf/vcardx#'.$p,
			'http://www.w3.org/2006/vcard/ns#'.$p,
			'http://www.w3.org/2001/vcard-rdf/3.0#'.uc($p));
		push @predicates, 'http://open.vocab.org/terms/calendar'
			if ($p eq 'caluri');
			
		my @vals = allOf($S, @predicates);
		foreach my $v (@vals)
		{
			if ($v !~ /^bnode\:/)
			{
				push @rv, [uc($p).';VALUE=uri', $v];
				next;
			}
	
			my $V = findSubject($triples, $v);
			if (Swignition::Export::Calendar::is($V))
			{
				my $calData = Swignition::Export::Calendar::outICS($triples, $V->uri);
				push @rv, [uc($p).';VALUE=uri', 'data:text/calendar;base64,'.encode_base64($calData)];
			}
		}
	}

	# Photo
	my @photos = allOf($S, 'urn:ietf:rfc:2426#photo',
		'http://www.w3.org/2006/vcard/ns#photo',
		'http://www.w3.org/2001/vcard-rdf/3.0#PHOTO',
		'http://xmlns.com/foaf/0.1/img',
		'http://xmlns.com/foaf/0.1/depiction');
	foreach my $photo (@photos)
		{ push @rv, ['PHOTO;VALUE=uri',$photo]; }

	# Logo
	my @logos = allOf($S, 'urn:ietf:rfc:2426#logo',
		'http://www.w3.org/2006/vcard/ns#logo',
		'http://www.w3.org/2001/vcard-rdf/3.0#LOGO',
		'http://xmlns.com/foaf/0.1/logo');
	foreach my $logo (@logos)
		{ push @rv, ['LOGO;VALUE=uri',$logo]; }

	# Sound
	my @sounds = allOf($S, 'urn:ietf:rfc:2426#sound',
		'http://www.w3.org/2006/vcard/ns#sound',
		'http://www.w3.org/2001/vcard-rdf/3.0#SOUND');
	foreach my $sound (@sounds)
		{ push @rv, ['SOUND;VALUE=uri',$sound]; }

	# Key (crypto)
	my @keys = allOf($S, 'urn:ietf:rfc:2426#key',
		'http://www.w3.org/2006/vcard/ns#key',
		'http://www.w3.org/2001/vcard-rdf/3.0#KEY',
		'http://xmlns.com/wot/0.1/pubkeyAddress');
	foreach my $key (@keys)
		{ push @rv, ['KEY;VALUE=uri',$key]; }
	
	# Keys (WOT RDF schema)
	my @keys = allOfObj($S, 'http://xmlns.com/wot/0.1/hasKey');
	foreach my $key (@keys)
	{
		my $keySubject = findSubject($triples, $key->toString);
		if ($keySubject)
		{
			my $uri = firstOf($keySubject, 'http://xmlns.com/wot/0.1/pubkeyAddress');
			push @rv, ['KEY;VALUE=uri', $uri]
				if ($uri);
		}
	}

	# Notes
	my @notes = allOfLiteral($S, 
		['urn:ietf:rfc:2426#note',
		'http://www.w3.org/2006/vcard/ns#note',
		'http://www.w3.org/2001/vcard-rdf/3.0#NOTE',
		'http://www.w3.org/2000/01/rdf-schema#comment',
		'http://purl.org/dc/terms/abstract',
		'http://purl.org/dc/terms/description',
		'http://purl.org/dc/elements/1.1/description',
		'http://xmlns.com/foaf/0.1/plan',
		'http://xmlns.com/foaf/0.1/geekcode'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $note (@notes)
		{ push @rv, ['NOTE',$note]; }

	# Agents
	my @agents = allOfObj($S,
		'urn:ietf:rfc:2426#agent',
		'http://www.w3.org/2006/vcard/ns#agent',
 		'http://www.w3.org/2001/vcard-rdf/3.0#AGENT');
	foreach my $agent (@agents)
	{
		my $agentSubject = findSubject($triples, $agent->toString);
		if ($agentSubject)
		{
			if (is($agentSubject))
			{
				my $a = outVCF($triples, $agentSubject->uri);
				push @rv, ['AGENT',$a];
			}
			else
			{
				my $a = firstOfLiteral($agentSubject,
					['urn:ietf:rfc:2426#fn',
					'http://www.w3.org/2006/vcard/ns#fn',
					'http://www.w3.org/2001/vcard-rdf/3.0#FN',
					'http://xmlns.com/foaf/0.1/fullName',
					'http://xmlns.com/foaf/0.1/name',
					'http://open.vocab.org/terms/fullName',
					'http://open.vocab.org/terms/informalName',
					'http://open.vocab.org/terms/formalName',
					'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
					'http://purl.org/dc/terms/title',
					'http://purl.org/dc/elements/1.1/title'],
					['urn:ietf:rfc:2426#value',
					'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
				push @rv, ['AGENT;VALUE=text',$a] if ($a);
			}
		}
		elsif ($agent->type)
		{
			push @rv, ['AGENT;VALUE=uri',$agent->toString];
		}
		else
		{
			my $a = $agent->toString;
			$a =~ s/(^\s+|\s+$)//g;
			$a =~ s/\s+/ /g;
			push @rv, ['AGENT;VALUE=text',$a] if ($a);
		}
	}

 	# Members
 	my @members = allOfObj($S,
 		'urn:ietf:rfc:2426#member',
 		'http://xmlns.com/foaf/0.1/member',
 		'http://buzzword.org.uk/rdf/vcardx#member');
 	foreach my $m (@members)
	{
		if ($m->type)
		{
			my $uri = $m->toString;
			if ($uri =~ /^bnode/)
			{
				my $memberSubject = findSubject($triples, $uri);
				$uri = firstOf($memberSubject,
						'urn:ietf:rfc:2426#uid',
						'http://www.w3.org/2006/vcard/ns#uid',
						'http://www.w3.org/2001/vcard-rdf/3.0#UID',
						'urn:ietf:rfc:2426#url',
						'http://www.w3.org/2006/vcard/ns#url',
						'http://xmlns.com/foaf/0.1/homepage',
						'http://xmlns.com/foaf/0.1/weblog',
						'http://www.w3.org/2000/10/swap/pim/contact#homepage',
						'http://www.w3.org/2000/10/swap/pim/contact#publicHomepage'
					);
			}
			
			push @rv, ['MEMBER',$uri]
				if ($uri);
		}
	}
	
	# TZ (timezone)
	my $tz = firstOfLiteral($S,
		['urn:ietf:rfc:2426#tz',
		'http://www.w3.org/2006/vcard/ns#tz',
		'http://www.w3.org/2001/vcard-rdf/3.0#TZ'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['TZ',$tz]
		if ($tz);

	# Bday (date of birth)
	my $bday = firstOfLiteral($S,
		['urn:ietf:rfc:2426#bday',
		'http://www.w3.org/2006/vcard/ns#bday',
		'http://www.w3.org/2001/vcard-rdf/3.0#BDAY',
		'http://www.w3.org/2000/10/swap/pim/contact#birthday',
		'http://xmlns.com/foaf/0.1/birthday'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['BDAY',$bday]
		if ($bday);
		
	# Dday (date of death)
	my $dday = firstOfLiteral($S,
		['urn:ietf:rfc:2426#dday',
		'http://buzzword.org.uk/rdf/vcardx#dday',
		'http://open.vocab.org/terms/dateOfDeath'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['DDAY',$dday]
		if ($dday);
		
	# Gender
	my $gender = firstOfLiteral($S,
		['urn:ietf:rfc:2426#gender',
		'http://buzzword.org.uk/rdf/vcardx#gender',
		'http://xmlns.com/foaf/0.1/gender'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	if ($gender)
	{
		push @rv, ['GENDER',$gender];
	}
	else
	{
		if (inProp($S, 'http://www.w3.org/2000/10/swap/pim/contact#Male',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))
		{
			push @rv, ['GENDER','M'];
		}

		if (inProp($S, 'http://www.w3.org/2000/10/swap/pim/contact#Female',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))
		{
			push @rv, ['GENDER','F'];
		}
	}
	
	# Categories
	my @cats = allOfObj($S, 'urn:ietf:rfc:2426#category',
		'http://www.w3.org/2006/vcard/ns#category',
		'http://www.w3.org/2001/vcard-rdf/3.0#CATEGORIES',
		'http://www.holygoat.co.uk/owl/redwood/0.1/tags/taggedWithTag');
	my $catstring = '';
	foreach my $cat (@cats)
	{
		my $catSubject = findSubject($triples, $cat->toString);
		if ($catSubject)
		{
			my $category = firstOf($catSubject,
				'http://www.holygoat.co.uk/owl/redwood/0.1/tags/name',
				'http://www.w3.org/1999/02/22-rdf-syntax-ns#value',
				'http://purl.org/dc/terms/description',
				'http://purl.org/dc/elements/1.1/description');
			$catstring .= ','.uc($category)
				if (length $category);
		}
		else
		{
			my $category = $cat->toString;
			$category =~ s/(^\s+|\s+$)//g;
			$category =~ s/\s+/ /g;
			$catstring .= ','.uc($category);
		}
	}
	$catstring =~ s/^\,//;
	push @rv, ['CATEGORIES',$catstring,1]
		if ($catstring);

	# Class
	my $class = uc(firstOfLiteral($S,
		['urn:ietf:rfc:2426#class',
		'http://www.w3.org/2006/vcard/ns#class',
		'http://www.w3.org/2001/vcard-rdf/3.0#CLASS'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']));
	$class =~ s/\s//g;
	push @rv, ['CLASS',$class]
		if ($class =~ /^(PUBLIC|PRIVATE|CONFIDENTIAL|X-)/);
		
	# Rev
	my @rev = allOfLiteral($S,
		['urn:ietf:rfc:2426#rev',
		'http://www.w3.org/2006/vcard/ns#rev',
		'http://www.w3.org/2001/vcard-rdf/3.0#REV',
		'http://purl.org/dc/terms/modified',
		'http://purl.org/dc/elements/1.1/modified'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	foreach my $rev (@rev)
		{ push @rv, ['REV',$rev]; }
		
	# Kind
	my $kind = firstOfLiteral($S,
		['urn:ietf:rfc:2426#kind',
		'http://buzzword.org.uk/rdf/vcardx#kind'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['KIND',$kind]
		if ($kind);
		
	# Lang
	my @langs = allOfLiteral($S, 
		['urn:ietf:rfc:2426#lang',
		'http://buzzword.org.uk/rdf/vcardx#lang',
		'http://www.w3.org/2000/10/swap/pim/contact#motherTongue'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['LANG', (join ',', @langs)]
		if (@langs);
		
	# Birth, place thereof - TODO embedded vcards, adr, geo
	my $birth = firstOfLiteral($S,
		['urn:ietf:rfc:2426#birth',
		'http://buzzword.org.uk/rdf/vcardx#birth'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['BIRTH',$birth]
		if ($birth);
		
	# Death, place thereof
	my $death = firstOfLiteral($S,
		['urn:ietf:rfc:2426#death',
		'http://buzzword.org.uk/rdf/vcardx#death'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	push @rv, ['DEATH',$death]
		if ($death);

	# Related
	my $rel;
	my @relationships = ('http://gmpg.org/xfn/11#contact',
		'http://gmpg.org/xfn/11#friend',
		'http://gmpg.org/xfn/11#acquaintance',
		'http://gmpg.org/xfn/11#met',
		'http://gmpg.org/xfn/11#co-worker',
		'http://gmpg.org/xfn/11#colleague',
		'http://gmpg.org/xfn/11#co-resident',
		'http://gmpg.org/xfn/11#neighbor',
		'http://gmpg.org/xfn/11#child',
		'http://gmpg.org/xfn/11#parent',
		'http://gmpg.org/xfn/11#sibling',
		'http://gmpg.org/xfn/11#spouse',
		'http://gmpg.org/xfn/11#kin',
		'http://gmpg.org/xfn/11#muse',
		'http://gmpg.org/xfn/11#crush',
		'http://gmpg.org/xfn/11#date',
		'http://gmpg.org/xfn/11#sweetheart',
		'http://vocab.sindice.com/xfn#contact',
		'http://vocab.sindice.com/xfn#friend',
		'http://vocab.sindice.com/xfn#acquaintance',
		'http://vocab.sindice.com/xfn#met',
		'http://vocab.sindice.com/xfn#co-worker',
		'http://vocab.sindice.com/xfn#colleague',
		'http://vocab.sindice.com/xfn#co-resident',
		'http://vocab.sindice.com/xfn#neighbor',
		'http://vocab.sindice.com/xfn#child',
		'http://vocab.sindice.com/xfn#parent',
		'http://vocab.sindice.com/xfn#sibling',
		'http://vocab.sindice.com/xfn#spouse',
		'http://vocab.sindice.com/xfn#kin',
		'http://vocab.sindice.com/xfn#muse',
		'http://vocab.sindice.com/xfn#crush',
		'http://vocab.sindice.com/xfn#date',
		'http://vocab.sindice.com/xfn#sweetheart',
		'http://xen.adactio.com/#nemesis',
		'http://xen.adactio.com/#enemy',
		'http://xen.adactio.com/#nuisance',
		'http://xen.adactio.com/#evil-twin',
		'http://xen.adactio.com/#rival',
		'http://xen.adactio.com/#fury',
		'http://xen.adactio.com/#creep',
		'http://buzzword.org.uk/rdf/xen#nemesis',
		'http://buzzword.org.uk/rdf/xen#enemy',
		'http://buzzword.org.uk/rdf/xen#nuisance',
		'http://buzzword.org.uk/rdf/xen#evil-twin',
		'http://buzzword.org.uk/rdf/xen#rival',
		'http://buzzword.org.uk/rdf/xen#fury',
		'http://buzzword.org.uk/rdf/xen#creep',
		'http://xmlns.com/foaf/0.1/knows',
		'http://purl.org/vocab/relationship/acquaintanceOf',
		'http://purl.org/vocab/relationship/ambivalentOf',
		'http://purl.org/vocab/relationship/ancestorOf',
		'http://purl.org/vocab/relationship/antagonistOf',
		'http://purl.org/vocab/relationship/apprenticeTo',
		'http://purl.org/vocab/relationship/childOf',
		'http://purl.org/vocab/relationship/closeFriendOf',
		'http://purl.org/vocab/relationship/collaboratesWith',
		'http://purl.org/vocab/relationship/colleagueOf',
		'http://purl.org/vocab/relationship/descendantOf',
		'http://purl.org/vocab/relationship/employedBy',
		'http://purl.org/vocab/relationship/employerOf',
		'http://purl.org/vocab/relationship/enemyOf',
		'http://purl.org/vocab/relationship/engagedTo',
		'http://purl.org/vocab/relationship/friendOf',
		'http://purl.org/vocab/relationship/grandchildOf',
		'http://purl.org/vocab/relationship/grandparentOf',
		'http://purl.org/vocab/relationship/hasMet',
		'http://purl.org/vocab/relationship/knowsByReputation',
		'http://purl.org/vocab/relationship/knowsInPassing',
		'http://purl.org/vocab/relationship/knowsOf',
		'http://purl.org/vocab/relationship/lifePartnerOf',
		'http://purl.org/vocab/relationship/livesWith',
		'http://purl.org/vocab/relationship/lostContactWith',
		'http://purl.org/vocab/relationship/mentorOf',
		'http://purl.org/vocab/relationship/neighborOf',
		'http://purl.org/vocab/relationship/parentOf',
		'http://purl.org/vocab/relationship/siblingOf',
		'http://purl.org/vocab/relationship/spouseOf',
		'http://purl.org/vocab/relationship/worksWith',
		'http://www.w3.org/2000/10/swap/pim/contact#partner',
		'http://open.vocab.org/terms/hearts');
	foreach my $relationship (@relationships)
	{
		my @related = allOfObj($S, $relationship);
		foreach my $r (@related)
		{
			my $uri;
			
			if ($r->{type})
			{
				my $R = findSubject($triples, $r->toString);
				if ($R)
				{
					my $uid = firstOf($R, 'urn:ietf:rfc:2426#uid',
						'http://www.w3.org/2006/vcard/ns#uid',
						'http://www.w3.org/2001/vcard-rdf/3.0#UID');
					if ($uid)
					{
						$uri = $uid;
						$rel->{$uri}->{VALUE} = ($uid =~ /^[a-z0-9]+\:/i)?'uri':'text';
					}
					else
					{
						my $fn = firstOfLiteral($R,
							['urn:ietf:rfc:2426#fn',
							'http://www.w3.org/2006/vcard/ns#fn',
							'http://www.w3.org/2001/vcard-rdf/3.0#FN',
							'http://xmlns.com/foaf/0.1/fullName',
							'http://xmlns.com/foaf/0.1/name',
							'http://open.vocab.org/terms/fullName',
							'http://open.vocab.org/terms/informalName',
							'http://open.vocab.org/terms/formalName',
							'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
							'http://purl.org/dc/terms/title',
							'http://purl.org/dc/elements/1.1/title'],
							['urn:ietf:rfc:2426#value',
							'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);	
						if ($fn)
						{
							$uri = $fn;
							$rel->{$uri}->{VALUE} = 'text';
						}
						else
						{
							$uri = $r->toString;
							$rel->{$uri}->{VALUE} = 'uri';
						}
					}
				}
				else
				{
					$uri = $r->toString;
					$rel->{$uri}->{VALUE} = 'uri';
				}
			}
			else
			{
				$uri = $r->toString;
				$rel->{$uri}->{VALUE} = 'text';
			}
			
			my $prop = $r->property->localname;
			
			if ($prop =~ /^(parentOf|child)$/)
				{ $rel->{$uri}->{TYPE}->{child} = 1; }
			elsif ($prop =~ /^(childOf|parent)$/)
				{ $rel->{$uri}->{TYPE}->{parent} = 1; }
			elsif ($prop =~ /^(siblingOf|sibling)$/)
				{ $rel->{$uri}->{TYPE}->{sibling} = 1; }
			elsif ($prop =~ /^(apprenticeTo|employedBy|mentorOf)$/)
				{ $rel->{$uri}->{TYPE}->{manager} = 1; }
			elsif ($prop =~ /^(employerOf)$/)
				{ $rel->{$uri}->{TYPE}->{assistant} = 1; }
			else
				{ $rel->{$uri}->{TYPE}->{'x-'.$prop} = 1; }	
		}
	}
	foreach my $uri (keys %$rel)
	{
		push @rv, [sprintf('RELATED;VALUE=%s;TYPE=%s', 
			$rel->{$uri}->{VALUE},
			(keys %{$rel->{$uri}->{TYPE}})), $uri];
	}

	# Done.
	push @rv, ['END','VCARD'];
	
	# Convert to text/directory syntax.
	my $rv = '';
	foreach my $line (@rv)
	{
		my ($k, $v, $soft) = @{$line};
		
		if ($k =~ /VALUE.uri$/i && $v =~ /^data\:/)
		{
			my $uri = URI->new($v);
			$v = encode_base64($uri->data, '');
			$k =~ s/VALUE.uri$/ENCODING\=b/i;
			$k .= ';TYPE='.$uri->media_type
				if ($uri->media_type);
		}
		
		$v =~ s/\\/\\\\/g;
		$v =~ s/\r?\n/\\n/g;
		unless ($soft)
		{
			$v =~ s/\;/\\\;/g;
			$v =~ s/\,/\\\,/g;
		}
		
		$rv .= "$k:$v\r\n";
	}
	return $rv;
}


sub outName
{
	my $triples = shift;
	my $subject = shift;
	my $S = $triples->{$subject};
	
	my $fn = firstOfLiteral($S,
		['urn:ietf:rfc:2426#fn',
		'http://www.w3.org/2006/vcard/ns#fn',
		'http://www.w3.org/2001/vcard-rdf/3.0#FN',
		'http://xmlns.com/foaf/0.1/fullName',
		'http://xmlns.com/foaf/0.1/name',
		'http://open.vocab.org/terms/fullName',
		'http://open.vocab.org/terms/informalName',
		'http://open.vocab.org/terms/formalName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs',
		'http://purl.invalid/stuff/atom/owl#name',
		'http://bblfish.net/work/atom-owl/2006-06-06/#name',
		'http://purl.org/dc/terms/title',
		'http://purl.org/dc/elements/1.1/title'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);	
	if (length $fn)
		{ return $fn; }

	my @names = allOf($S, 'urn:ietf:rfc:2426#n',
		'http://www.w3.org/2006/vcard/ns#n',
		'http://www.w3.org/2001/vcard-rdf/3.0#N');
	push @names, $S->uri;

	foreach my $n (@names)
	{
		my $N = findSubject($triples, $n);
	
		# Name components
		my $honorific_pfx = join ' ', allOfLiteral($N,
			['urn:ietf:rfc:2426#honorific-prefix',
			'http://www.w3.org/2006/vcard/ns#honorific-prefix',
			'http://www.w3.org/2001/vcard-rdf/3.0#Prefix',
			'http://xmlns.com/foaf/0.1/title',
			'http://www.w3.org/2000/10/swap/pim/contact#personalTitle'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
		my $firstname = join ' ', allOfLiteral($N,
			['urn:ietf:rfc:2426#given-name',
			'http://www.w3.org/2006/vcard/ns#given-name',
			'http://www.w3.org/2001/vcard-rdf/3.0#Given',
			'http://xmlns.com/foaf/0.1/givenname',
			'http://www.w3.org/2000/10/swap/pim/contact#givenName',
			'http://www.w3.org/2000/10/swap/pim/contact#firstName'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
		my $surname = join ' ', allOfLiteral($N,
			['urn:ietf:rfc:2426#family-name',
			'http://www.w3.org/2006/vcard/ns#family-name',
			'http://www.w3.org/2001/vcard-rdf/3.0#Family',
			'http://xmlns.com/foaf/0.1/family_name',
			'http://xmlns.com/foaf/0.1/surname',
			'http://www.w3.org/2000/10/swap/pim/contact#lastName'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
		my $honorific_sfx = join ' ', allOfLiteral($N,
			['urn:ietf:rfc:2426#honorific-suffix',
			'http://www.w3.org/2006/vcard/ns#honorific-suffix',
			'http://www.w3.org/2001/vcard-rdf/3.0#Suffix',
			'http://www.w3.org/2000/10/swap/pim/contact#personalSuffix'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
	
		$fn = "$honorific_pfx $firstname $surname $honorific_sfx";
		$fn =~ s/(^\s+|\s+$)//g;
		$fn =~ s/\s+/ /g;
		if (length $fn)
			{ return $fn; }
	}
	
	# Nickname
	return firstOfLiteral($S, 
		['urn:ietf:rfc:2426#nickname',
		'http://www.w3.org/2006/vcard/ns#nickname',
		'http://www.w3.org/2001/vcard-rdf/3.0#NICKNAME',
		'http://xmlns.com/foaf/0.1/nick',
		'http://open.vocab.org/terms/familiarName',
		'http://www.w3.org/2000/10/swap/pim/contact#knownAs'],
		['urn:ietf:rfc:2426#value',
		'http://www.w3.org/1999/02/22-rdf-syntax-ns#value']);
}


sub outN
{
	my $triples = shift;
	my $subject = shift;
	my $fmt     = shift || 'vcf';
	my $S = $triples->{$subject};
	
	my @names = allOf($S, 'urn:ietf:rfc:2426#n',
		'http://www.w3.org/2006/vcard/ns#n',
		'http://www.w3.org/2001/vcard-rdf/3.0#N');
	push @names, "".$S->uri;
	
	foreach my $n (@names)
	{
		my $N = findSubject($triples, $n);
	
		# Name components
		my %name;
		$name{'family-name'} = [reverse allOfLiteral($N,
			['urn:ietf:rfc:2426#family-name',
			'http://www.w3.org/2006/vcard/ns#family-name',
			'http://www.w3.org/2001/vcard-rdf/3.0#Family',
			'http://xmlns.com/foaf/0.1/family_name',
			'http://xmlns.com/foaf/0.1/surname',
			'http://www.w3.org/2000/10/swap/pim/contact#lastName'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];
		$name{'given-name'} = [reverse allOfLiteral($N,
			['urn:ietf:rfc:2426#given-name',
			'http://www.w3.org/2006/vcard/ns#given-name',
			'http://www.w3.org/2001/vcard-rdf/3.0#Given',
			'http://xmlns.com/foaf/0.1/givenname',
			'http://www.w3.org/2000/10/swap/pim/contact#firstName',
			'http://www.w3.org/2000/10/swap/pim/contact#givenName',
			'urn:ietf:rfc:2426#initials'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];
		$name{'additional-name'} = [reverse allOfLiteral($N, 
			['urn:ietf:rfc:2426#additional-name',
			'http://www.w3.org/2006/vcard/ns#additional-name',
			'http://www.w3.org/2001/vcard-rdf/3.0#Other'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];
		$name{'honorific-prefix'} = [reverse allOfLiteral($N,
			['urn:ietf:rfc:2426#honorific-prefix',
			'http://www.w3.org/2006/vcard/ns#honorific-prefix',
			'http://www.w3.org/2001/vcard-rdf/3.0#Prefix',
			'http://xmlns.com/foaf/0.1/title',
			'http://www.w3.org/2000/10/swap/pim/contact#personalTitle'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];
		$name{'honorific-suffix'} = [reverse allOfLiteral($N,
			['urn:ietf:rfc:2426#honorific-suffix',
			'http://www.w3.org/2006/vcard/ns#honorific-suffix',
			'http://www.w3.org/2001/vcard-rdf/3.0#Suffix',
			'http://www.w3.org/2000/10/swap/pim/contact#personalSuffix'],
			['urn:ietf:rfc:2426#value',
			'http://www.w3.org/1999/02/22-rdf-syntax-ns#value'])];
	
		my $value;
		foreach my $prop (qw(family-name given-name additional-name
			honorific-prefix honorific-suffix))
		{
			my $v = join 'XCOMMAX', @{ $name{$prop} };
			$value .= $v.'XSEMICOLONX';
		}
		$value =~ s/\\/\\\\/g;
		$value =~ s/\;/\\\;/g;
		$value =~ s/\,/\\\,/g;
		$value =~ s/XCOMMAX/\,/g;
		$value =~ s/XSEMICOLONX/\;/g;
		$value =~ s/\;+$//g;
		$name{value} = $value;
		
		if (length $value)
		{
			return \%name if ($fmt eq 'jcard');		
			return $value;
		}
	}
	
}


# Implements the atom "Person" construct, though actually we may be an
# organisation or other type of contact.
sub _AtomPerson
{
	my $triples = shift;
	my $subject = shift;
	my $atomtag = shift;
	my $indent;
	my $S = $triples->{$subject};

	$atomtag =~ /^(\s*)<(.*)>$/;
	$indent  = $1;
	$atomtag = $2;

	my $name  = outName($triples, $subject);
	my $email = firstOf($S,
		'http://purl.invalid/stuff/atom/owl#email',
		'http://bblfish.net/work/atom-owl/2006-06-06/#email',
		'urn:ietf:rfc:2426#email',
		'http://www.w3.org/2006/vcard/ns#email',
		'http://www.w3.org/2006/vcard/ns#workEmail',
		'http://www.w3.org/2006/vcard/ns#mobileEmail',
		'http://www.w3.org/2006/vcard/ns#unlabelledEmail',
		'http://www.w3.org/2001/vcard-rdf/3.0#EMAIL',
		'http://www.w3.org/2000/10/swap/pim/contact#emailAddress',
		'http://xmlns.com/foaf/0.1/mbox',
		'http://xmlns.com/foaf/0.1/mbox_sha1sum');
	my $url   = firstOf($S, 
		'http://purl.invalid/stuff/atom/owl#uri',
		'http://bblfish.net/work/atom-owl/2006-06-06/#uri',
		'urn:ietf:rfc:2426#url',
		'http://www.w3.org/2006/vcard/ns#url',
		'http://xmlns.com/foaf/0.1/homepage',
		'http://xmlns.com/foaf/0.1/weblog',
		'http://xmlns.com/foaf/0.1/workplaceHomepage',
		'http://xmlns.com/foaf/0.1/workInfoHomepage',
		'http://xmlns.com/foaf/0.1/schoolHomepage',
		'http://xmlns.com/foaf/0.1/page',
		'http://xmlns.com/foaf/0.1/tipjar',
		'http://www.w3.org/2000/10/swap/pim/contact#homepage',
		'http://www.w3.org/2000/10/swap/pim/contact#publicHomepage');
	
	if ($email !~ /^mailto/)
	{
		my $Email = findSubject($triples, $email);
		if ($Email)
		{
			$email = firstOf($Email, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#value');
		}
	}
	
	$email =~ s/(^mailto:|\?.*$)//g;
	
	my $rv = "$indent<$atomtag\n$indent    xmlns:vcard=\"http://www.w3.org/2006/vcard/ns#\"\n$indent    xmlns:foaf=\"http://xmlns.com/foaf/0.1/\">\n";
	$rv .=   "$indent\t<name>".encode_entities_numeric($name)."</name>\n" if ($name);
	$rv .=   "$indent\t<email>".encode_entities_numeric($email)."</email>\n" if ($email);
	$rv .=   "$indent\t<uri>".encode_entities_numeric($url)."</uri>\n" if ($url);
	foreach my $p ($S->properties)
	{
		next if ($p eq 'http://www.w3.org/2006/vcard/ns#n'   # redundant and ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#fn'     # redundant
			|| $p eq 'http://www.w3.org/2006/vcard/ns#url'    # redundant
			|| $p eq 'http://www.w3.org/2006/vcard/ns#email'  # redundant and ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#adr'    # ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#geo'    # ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#agent'  # ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#org'    # ugly
			|| $p eq 'http://www.w3.org/2006/vcard/ns#tel'    # ugly
			);
			
		my $P = $S->property($p);
		if ($P->namespace eq 'http://www.w3.org/2006/vcard/ns#'
		||  $P->namespace eq 'http://xmlns.com/foaf/0.1/')
		{
			my $ns = 'vcard';
			$ns = 'foaf' if ($P->namespace eq 'http://xmlns.com/foaf/0.1/');
			
			foreach my $V (@{ $P->valObjects })
			{
				next if $V->type;
				my $v = $V->toString;
				$rv .= "$indent\t<$ns:".$P->localname.">".encode_entities_numeric($v)
					."</$ns:".$P->localname.">\n";
			}
		}
	}
	$rv .= "$indent</$atomtag>\n";
	return $rv;
}

1;
